﻿using System;
using System.Collections.Generic;
using System.Text;

namespace IMMeDotNet {

	/// <summary>
	/// The base class for incoming or outgoing messages.
	/// </summary>
	abstract class UsbMessage {
		
		/// <summary>
		/// Gets or sets the first ID byte of the <see cref="Message"/>.
		/// </summary>
		public byte ID1 { get; set; }
		/// <summary>
		/// Gets or sets the second ID byte of the <see cref="Message"/>.
		/// </summary>
		public byte ID2 { get; set; }
		/// <summary>
		/// Gets or sets the class ID byte of the <see cref="Message"/>.
		/// </summary>
		public byte MessageClass { get; set; }
		
		/// <summary>
		/// Returns a byte array of the underlying message contents.
		/// </summary>
		/// <returns>The underlying message contents as an array.</returns>
		public abstract byte[] ToArray();

		/// <summary>
		/// Converts the message into a string representation.
		/// </summary>
		public override string ToString() {
			var data = this.ToArray();
			StringBuilder result = new StringBuilder(10 + data.Length * 4);
			result.AppendFormat("<ID1={0:X2},ID2={1:X2},Class={2:X2}>", this.ID1, this.ID2, this.MessageClass);
			foreach (var item in data) result.AppendFormat("<{0:X2}>", item);
			return result.ToString();
		}
	}

	/// <summary>
	/// Represents a message that has been received from the wireless USB adaptor.
	/// </summary>
	class IncomingUsbMessage : UsbMessage {

		/// <summary>
		/// Stores the message contents.
		/// </summary>
		private byte[] data;
		
		/// <summary>
		/// Gets the length of the data associated with the message contents.
		/// </summary>
		public int Length {
			get {
				return this.data.Length;
			}
		}
		
		private int position;
		/// <summary>
		/// Gets or sets the read position within the message contents.
		/// </summary>
		public int Position {
			get {
				return this.position;
			}
			set {
				if (value >= 0 && position < this.data.Length) {
					this.position = value;
				} else {
					throw new IndexOutOfRangeException();
				}
			}
		}

		/// <summary>
		/// Creates an instance of a <see cref="IncomingUsbMessage"/> from its raw data.
		/// </summary>
		/// <param name="rawData">The raw incoming data that forms the message.</param>
		public IncomingUsbMessage(byte[] rawData) {
			this.ID1 = rawData[1];
			this.ID2 = rawData[2];
			this.MessageClass = rawData[3];
			this.data = new byte[rawData[0] - 3];
			Array.Copy(rawData, 4, this.data, 0, this.data.Length);
		}

		/// <summary>
		/// Reads a byte from the message.
		/// </summary>
		/// <returns>The byte read from the message.</returns>
		public byte ReadByte() {
			return this.data[this.position++];
		}

		/// <summary>
		/// Reads an unsigned 32-bit integer from the message.
		/// </summary>
		/// <returns>The unsigned 32-bit integer read from the message.</returns>
		public uint ReadUInt32() {
			var result = BitConverter.ToUInt32(this.data, this.position);
			this.position += 4;
			return result;
		}

		/// <summary>
		/// Reads an IMMeString from the message.
		/// </summary>
		/// <returns>The string read from the message.</returns>
		public IMMeString ReadString() {
			var stringContents = new List<byte>(32);
			byte stringCharacter;
			while ((stringCharacter = this.data[this.position++]) != 0) stringContents.Add(stringCharacter);
			return new IMMeString(stringContents.ToArray());
		}

		/// <summary>
		/// Reads a <see cref="MessageTarget"/> from the message.
		/// </summary>
		/// <returns>The <see cref="MessageTarget"/> read from the message.</returns>
		public MessageTarget ReadTarget() {
			return new MessageTarget(this.ReadByte(), this.ReadByte(), this.ReadUInt32());
		}


		/// <summary>
		/// Creates an empty outgoing message with its ID bytes configured as a response to the <see cref="IncomingMessage"/>.
		/// </summary>
		/// <returns>The <see cref="OutgoingMessage"/> to be used as in response.</returns>
		public OutgoingUsbMessage CreateResponse() {
			return new OutgoingUsbMessage(0x00, (byte)(this.ID2 + 1), this.MessageClass);
		}

		/// <summary>
		/// Returns a byte array of the underlying message contents.
		/// </summary>
		/// <returns>The underlying message contents as an array.</returns>
		public override byte[] ToArray() {
			var result = new byte[this.data.Length];
			Array.Copy(this.data, result, result.Length);
			return result;
		}

		/// <summary>
		/// Converts the message into a string representation.
		/// </summary>
		public override string ToString() {
			return "{In:" + base.ToString() + "}";
		}

	}

	/// <summary>
	/// Represents a message that is to be sent to the wireless USB adaptor.
	/// </summary>
	class OutgoingUsbMessage : UsbMessage {

		/// <summary>
		/// Stores the message contents.
		/// </summary>
		private List<byte> data;

		/// <summary>
		/// Creates an empty instance of an <see cref="OutgoingUsbMessage"/>.
		/// </summary>
		public OutgoingUsbMessage() {
			this.data = new List<byte>(32);
		}

		/// <summary>
		/// Creates an instance of a <see cref="OutgoingUsbMessage"/> with specific IDs.
		/// </summary>
		public OutgoingUsbMessage(byte id1, byte id2, byte messageClass)
			: this() {
			this.ID1 = id1;
			this.ID2 = id2;
			this.MessageClass = messageClass;
		}

		/// <summary>
		/// Writes a byte to the message.
		/// </summary>
		/// <param name="value">The value to write to the message.</param>
		public void Write(byte value) {
			this.data.Add(value);
		}

		/// <summary>
		/// Writes an error to the message.
		/// </summary>
		/// <param name="value">The error to write to the message.</param>
		public void Write(Error value) {
			this.data.Add((byte)value);
		}

		/// <summary>
		/// Writes an unsigned 32-bit integer to the message.
		/// </summary>
		/// <param name="value">The value to write to the message.</param>
		public void Write(uint value) {
			this.data.AddRange(BitConverter.GetBytes(value));
		}

		/// <summary>
		/// Writes an <see cref="IMMeString"/> to the message.
		/// </summary>
		/// <param name="value">The <see cref="IMMeString"/> to write to the message.</param>
		public void Write(IMMeString value) {
			this.data.AddRange(value.ToByteArray());
			this.data.Add(0);
		}

		/// <summary>
		/// Writes a string to the message.
		/// </summary>
		/// <param name="value">The string to write to the message.</param>
		public void Write(string value) {
			this.Write(new IMMeString(value));
		}

		/// <summary>
		/// Writes a <see cref="MessageTarget"/> to the message.
		/// </summary>
		/// <param name="target">The target of the message.</param>
		public void Write(MessageTarget target) {
			this.Write(target.MessageID);
			this.Write(target.ConnectionID);
			this.Write(target.ContactID);
		}

		/// <summary>
		/// Splits a message into multiple packets.
		/// </summary>
		/// <returns>An array of packets.</returns>
		public Packet[] ToPackets() {
			var rawBytes = new byte[this.data.Count + 4];
			if (rawBytes.Length > 255) throw new InvalidOperationException();
			rawBytes[0] = (byte)(rawBytes.Length - 1);
			rawBytes[1] = this.ID1;
			rawBytes[2] = this.ID2;
			rawBytes[3] = this.MessageClass;
			Array.Copy(this.data.ToArray(), 0, rawBytes, 4, this.data.Count);
			var maxPacketDataSize = 0x3C - 3;
			var packets = new List<Packet>();
			for (int i = 0; i < rawBytes.Length; i += maxPacketDataSize) {
				var p = new Packet();
				packets.Add(p);
				var packetDataLength = Math.Min(rawBytes.Length - i, maxPacketDataSize);
				p.Data = new byte[packetDataLength];
				Array.Copy(rawBytes, i, p.Data, 0, packetDataLength);
			}
			for (int i = 0; i < packets.Count; ++i) {
				packets[i].PartCount = (byte)packets.Count;
				packets[i].PartNumber = (byte)(i + 1);
				packets[i].Checksum = packets[i].CalculateChecksum();
			}
			return packets.ToArray();
		}

		/// <summary>
		/// Returns a byte array of the underlying message contents.
		/// </summary>
		/// <returns>The underlying message contents as an array.</returns>
		public override byte[] ToArray() {
			return this.data.ToArray();
		}

		/// <summary>
		/// Converts the message into a string representation.
		/// </summary>
		public override string ToString() {
			return "{Out:" + base.ToString() + "}";
		}

	}
}
